拾遗 - 多线程

对比

  • GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象
  • GCD只支持FIFO队列
  • NSOperationQueue可设置最大并发数、设置优先级、设置依赖关系等调整执行顺序
  • NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销

死锁

一定是发生在一个或多个线程之间的。那么死锁和线程阻塞的关系呢,可以这么理解,双向的阻塞导致了死锁

多个进程因循环等待资源而造成无法执行的现象

主队列同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override func viewDidLoad() {
super.viewDidLoad()

DispatchQueue.main.sync {
print("deadlock")
}
}
或者
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

dispatch_sync(serialQueue, ^{

NSLog(@"deadlock");
});
});

解决: 异步或者其它队列

GCD

执行顺序:串行队列先异步后同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

NSLog(@"1");

dispatch_async(serialQueue, ^{

NSLog(@"2");
});

NSLog(@"3");

dispatch_sync(serialQueue, ^{

NSLog(@"4");
});

NSLog(@"5");

打印顺序是13245

首先先打印1
接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5

所以最终顺序就是13245。
这里的任务4在主线程中执行,而任务2在子线程中执行。
如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行(可以添加多个任务看效果)

dispatch_barrier_sync(栅栏函数)

场景多读单写:可以多个读者同时读取数据,而在读的时候,不能取写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (id)readDataForKey:(NSString *)key
{
__block id result;

dispatch_sync(_concurrentQueue, ^{

result = [self valueForKey:key];
});

return result;
}

- (void)writeData:(id)data forKey:(NSString *)key
{
dispatch_barrier_async(_concurrentQueue, ^{

[self setValue:data forKey:key];
});
}

dispatch_group_async

场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

for (NSInteger i = 0; i < 10; i++) {

dispatch_group_async(group, concurrentQueue, ^{

sleep(1);

NSLog(@"%zd:网络请求",i);
});
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

NSLog(@"刷新页面");
});

Dispatch Semaphore(信号量)

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

dispatch_after(延时函数)

dispatch_once(单例)

面试题

执行顺序

NSThread+runloop实现常驻线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+ (NSThread *)shareThread {

static NSThread *shareThread = nil;

static dispatch_once_t oncePredicate;

dispatch_once(&oncePredicate, ^{

shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest2) object:nil];

[shareThread setName:@"threadTest"];

[shareThread start];
});

return shareThread;
}

+ (void)threadTest
{
@autoreleasepool {

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];
}
}

自旋锁

是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

互斥锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

对比

自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
  缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

  • 自旋锁:atomic、OSSpinLock、dispatch_semaphore_t
  • 互斥锁:pthread_mutex、@synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock

atomic和nonatomic的对比

  1. atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。
  2. atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的,比如上面的例子。
    也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。
    atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。
  3. nonatomic:就没有这个保证了,nonatomic返回你的对象可能就不是完整的value。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。但仅仅使用atomic并不会使得对象线程安全,我们还要为对象线程添加lock来确保线程的安全。
  4. nonatomic的速度要比atomic的快。
  5. atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同